import { ready as cryptoReady, tcrypto, utils } from '@tanker/crypto';
import { expect } from '@tanker/test-utils';

import {
  serializeUserDeviceV3,
  unserializeUserDeviceV1,
  unserializeUserDeviceV2,
  unserializeUserDeviceV3,
} from '../Serialize';
import type {
  DeviceCreationRecord,
} from '../Serialize';

import { makeUint8Array } from '../../__tests__/makeUint8Array';

// NOTE: If you ever have to change something here, change it in the Go code too!
// The test vectors should stay the same
describe('user serialization: payload test vectors', () => {
  before(() => cryptoReady);

  it('correctly deserializes a DeviceCreation v1 test vector', async () => {
    const deviceCreation = {
      ephemeral_public_signature_key: new Uint8Array([
        0x4e, 0x2a, 0x65, 0xdf, 0xe6, 0x5d, 0x00, 0x58, 0xf4, 0xdf, 0xb0, 0x5d,
        0x37, 0x64, 0x18, 0x1d, 0x10, 0x61, 0xf7, 0x54, 0xbb, 0x70, 0x30, 0x4f,
        0x08, 0x6e, 0x32, 0x14, 0x85, 0x7a, 0xee, 0xe5,
      ]),
      user_id: new Uint8Array([
        0xbd, 0xec, 0xe7, 0xbe, 0x4c, 0xd6, 0xc8, 0x33, 0xec, 0xf9, 0x42, 0xe1,
        0xa9, 0xc4, 0xa7, 0x3e, 0x39, 0xac, 0xdd, 0x6d, 0x99, 0x37, 0xc2, 0x9a,
        0xbf, 0xf8, 0x6c, 0x4f, 0xce, 0x3a, 0x34, 0xcd,
      ]),
      delegation_signature: new Uint8Array([
        0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
        0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
        0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
        0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
        0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
        0xAA, 0xAA, 0xAA, 0xAA,
      ]),
      public_signature_key: new Uint8Array([
        0x21, 0x2c, 0x54, 0x3a, 0xae, 0xcf, 0xc6, 0xef, 0x0b, 0x60, 0xae, 0xe6,
        0x11, 0x52, 0xa1, 0x30, 0x60, 0xbc, 0x34, 0xbc, 0x1b, 0x89, 0x39, 0xe1,
        0xd9, 0x94, 0x9a, 0xaa, 0x14, 0x4c, 0x41, 0x60,
      ]),
      public_encryption_key: new Uint8Array([
        0x42, 0x9a, 0xfa, 0x09, 0xee, 0xea, 0xce, 0x12, 0xec, 0x59, 0x06, 0x35,
        0xa8, 0x7f, 0x82, 0xe6, 0x39, 0xc8, 0xce, 0xd0, 0xc8, 0xe5, 0x57, 0x16,
        0x72, 0x94, 0x9e, 0xfb, 0xed, 0x59, 0xde, 0x2e,
      ]),
      user_key_pair: null,
      is_ghost_device: false,
      last_reset: new Uint8Array(tcrypto.HASH_SIZE),
      revoked: Number.MAX_SAFE_INTEGER,
    };

    const payload = utils.concatArrays(
      deviceCreation.ephemeral_public_signature_key,
      deviceCreation.user_id,
      deviceCreation.delegation_signature,
      deviceCreation.public_signature_key,
      deviceCreation.public_encryption_key,
    );

    expect(unserializeUserDeviceV1(payload)).to.deep.equal(deviceCreation);
  });

  it('correctly deserializes a DeviceCreation v2 test vector', async () => {
    const deviceCreation = {
      last_reset: new Uint8Array(tcrypto.HASH_SIZE),
      ephemeral_public_signature_key: new Uint8Array([
        0x4e, 0x2a, 0x65, 0xdf, 0xe6, 0x5d, 0x00, 0x58, 0xf4, 0xdf, 0xb0, 0x5d, 0x37, 0x64, 0x18, 0x1d,
        0x10, 0x61, 0xf7, 0x54, 0xbb, 0x70, 0x30, 0x4f, 0x08, 0x6e, 0x32, 0x14, 0x85, 0x7a, 0xee, 0xe5,
      ]),
      user_id: new Uint8Array([
        0xbd, 0xec, 0xe7, 0xbe, 0x4c, 0xd6, 0xc8, 0x33, 0xec, 0xf9, 0x42, 0xe1, 0xa9, 0xc4, 0xa7, 0x3e,
        0x39, 0xac, 0xdd, 0x6d, 0x99, 0x37, 0xc2, 0x9a, 0xbf, 0xf8, 0x6c, 0x4f, 0xce, 0x3a, 0x34, 0xcd,
      ]),
      delegation_signature: new Uint8Array([
        0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
        0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
        0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
        0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
      ]),
      public_signature_key: new Uint8Array([
        0x21, 0x2c, 0x54, 0x3a, 0xae, 0xcf, 0xc6, 0xef, 0x0b, 0x60, 0xae, 0xe6, 0x11, 0x52, 0xa1, 0x30,
        0x60, 0xbc, 0x34, 0xbc, 0x1b, 0x89, 0x39, 0xe1, 0xd9, 0x94, 0x9a, 0xaa, 0x14, 0x4c, 0x41, 0x60,
      ]),
      public_encryption_key: new Uint8Array([
        0x42, 0x9a, 0xfa, 0x09, 0xee, 0xea, 0xce, 0x12, 0xec, 0x59, 0x06, 0x35, 0xa8, 0x7f, 0x82, 0xe6,
        0x39, 0xc8, 0xce, 0xd0, 0xc8, 0xe5, 0x57, 0x16, 0x72, 0x94, 0x9e, 0xfb, 0xed, 0x59, 0xde, 0x2e,
      ]),
      user_key_pair: null,
      is_ghost_device: false,
      revoked: Number.MAX_SAFE_INTEGER,
    };

    const payload = utils.concatArrays(
      deviceCreation.last_reset,
      deviceCreation.ephemeral_public_signature_key,
      deviceCreation.user_id,
      deviceCreation.delegation_signature,
      deviceCreation.public_signature_key,
      deviceCreation.public_encryption_key,
    );

    expect(unserializeUserDeviceV2(payload)).to.deep.equal(deviceCreation);
  });

  it('correctly deserializes a DeviceCreation v3 test vector', async () => {
    const payload = new Uint8Array([
      // ephemeral_public_signature_key
      0x65, 0x70, 0x68, 0x20, 0x70, 0x75, 0x62, 0x20, 0x6b, 0x65, 0x79, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      // user_id
      0x75, 0x73, 0x65, 0x72, 0x20, 0x69, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      // delegation_signature
      0x64, 0x65, 0x6c, 0x65, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x73, 0x69, 0x67, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      // public_signature_key
      0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x20, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65,
      0x20, 0x6b, 0x65, 0x79, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      // public_encryption_key
      0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x20, 0x65, 0x6e, 0x63, 0x20, 0x6b, 0x65, 0x79, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      // user public_encryption_key
      0x75, 0x73, 0x65, 0x72, 0x20, 0x70, 0x75, 0x62, 0x20, 0x65, 0x6e, 0x63, 0x20, 0x6b, 0x65, 0x79,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      // user encrypted_private_encryption_key
      0x75, 0x73, 0x65, 0x72, 0x20, 0x65, 0x6e, 0x63, 0x20, 0x6b, 0x65, 0x79, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      // is_ghost_device
      0x01,
    ]);

    const deviceCreation = {
      last_reset: new Uint8Array(32),
      ephemeral_public_signature_key: makeUint8Array('eph pub key', tcrypto.SIGNATURE_PUBLIC_KEY_SIZE),
      user_id: makeUint8Array('user id', tcrypto.HASH_SIZE),
      delegation_signature: makeUint8Array('delegation sig', tcrypto.SIGNATURE_SIZE),
      public_signature_key: makeUint8Array('public signature key', tcrypto.SIGNATURE_PUBLIC_KEY_SIZE),
      public_encryption_key: makeUint8Array('public enc key', tcrypto.ENCRYPTION_PUBLIC_KEY_SIZE),
      user_key_pair: {
        public_encryption_key: makeUint8Array('user pub enc key', tcrypto.ENCRYPTION_PUBLIC_KEY_SIZE),
        encrypted_private_encryption_key: makeUint8Array('user enc key', tcrypto.SEALED_KEY_SIZE),
      },
      is_ghost_device: true,
      revoked: Number.MAX_SAFE_INTEGER,
    };

    expect(unserializeUserDeviceV3(payload)).to.deep.equal(deviceCreation);
  });
});

describe('user serialization: payloads', () => {
  it('should serialize/unserialize a UserDeviceV3', async () => {
    const ephemeralKeys = tcrypto.makeSignKeyPair();
    const signatureKeys = tcrypto.makeSignKeyPair();
    const encryptionKeys = tcrypto.makeEncryptionKeyPair();
    const userDevice = {
      last_reset: new Uint8Array(tcrypto.HASH_SIZE),
      ephemeral_public_signature_key: ephemeralKeys.publicKey,
      user_id: utils.fromString('12341234123412341234123412341234'),
      delegation_signature: utils.fromString('1234123412341234123412341234123412341234123412341234123412341234'),
      public_signature_key: signatureKeys.publicKey,
      public_encryption_key: encryptionKeys.publicKey,
      user_key_pair: {
        public_encryption_key: makeUint8Array('user pub enc key', tcrypto.ENCRYPTION_PUBLIC_KEY_SIZE),
        encrypted_private_encryption_key: makeUint8Array('user enc priv key', tcrypto.SEALED_KEY_SIZE),
      },
      is_ghost_device: true,
      revoked: Number.MAX_SAFE_INTEGER,
    };

    expect(unserializeUserDeviceV3(serializeUserDeviceV3(userDevice))).to.deep.equal(userDevice);
  });

  it('should throw if the last reset is not null when serializing a new userDeviceV3', async () => {
    const ephemeralKeys = tcrypto.makeSignKeyPair();
    const signatureKeys = tcrypto.makeSignKeyPair();
    const encryptionKeys = tcrypto.makeEncryptionKeyPair();
    const userDevice = {
      last_reset: new Uint8Array(Array.from({
        length: tcrypto.HASH_SIZE,
      }, () => 1)),
      ephemeral_public_signature_key: ephemeralKeys.publicKey,
      user_id: utils.fromString('12341234123412341234123412341234'),
      delegation_signature: utils.fromString('1234123412341234123412341234123412341234123412341234123412341234'),
      public_signature_key: signatureKeys.publicKey,
      public_encryption_key: encryptionKeys.publicKey,
      user_key_pair: {
        public_encryption_key: makeUint8Array('user pub enc key', tcrypto.ENCRYPTION_PUBLIC_KEY_SIZE),
        encrypted_private_encryption_key: makeUint8Array('user enc priv key', tcrypto.SEALED_KEY_SIZE),
      },
      is_ghost_device: true,
      revoked: Number.MAX_SAFE_INTEGER,
    };
    expect(() => serializeUserDeviceV3(userDevice)).to.throw();
  });

  describe('serialization of invalid user device', () => {
    let userDevice: DeviceCreationRecord;

    beforeEach(() => {
      userDevice = {
        last_reset: new Uint8Array(tcrypto.HASH_SIZE),
        ephemeral_public_signature_key: new Uint8Array(tcrypto.SIGNATURE_PUBLIC_KEY_SIZE),
        user_id: new Uint8Array(tcrypto.HASH_SIZE),
        delegation_signature: new Uint8Array(tcrypto.SIGNATURE_SIZE),
        public_signature_key: new Uint8Array(tcrypto.SIGNATURE_PUBLIC_KEY_SIZE),
        public_encryption_key: new Uint8Array(tcrypto.SIGNATURE_PUBLIC_KEY_SIZE),
        user_key_pair: {
          public_encryption_key: new Uint8Array(tcrypto.ENCRYPTION_PUBLIC_KEY_SIZE),
          encrypted_private_encryption_key: new Uint8Array(tcrypto.SEALED_KEY_SIZE),
        },
        is_ghost_device: true,
        revoked: Number.MAX_SAFE_INTEGER,
      };
    });
    const fields = [
      'ephemeral_public_signature_key',
      'user_id',
      'delegation_signature',
      'public_signature_key',
      'public_encryption_key',
    ];
    fields.forEach(field => {
      it(`should throw if user device with invalid ${field}`, async () => {
        // @ts-expect-error fields only contains Uint8Array fields from DeviceCreationRecord
        userDevice[field] = new Uint8Array(0);
        expect(() => serializeUserDeviceV3(userDevice)).to.throw();
      });
    });

    it('should throw if user device with invalid encrypted_private_encryption_key', async () => {
      userDevice.user_key_pair.encrypted_private_encryption_key = new Uint8Array(0);
      expect(() => serializeUserDeviceV3(userDevice)).to.throw();
    });

    it('should throw if user device with invalid public_encryption_key', () => {
      userDevice.user_key_pair.public_encryption_key = new Uint8Array(0);
      expect(() => serializeUserDeviceV3(userDevice)).to.throw();
    });
  });
});
